﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using System.Xml;
using System.Text;
using System.Net;
using Google.GData.Client;
using Google.GData.Extensions;
using Google.GData.Calendar;
using System.Collections;
using Microsoft.WindowsMobile.PocketOutlook;
using System.Security.Cryptography;


namespace RareEdge
{
	public class GMobileSync
	{
		#region Private Variables

		static readonly string dataFile = "GMobileSync.xml";

		private string _username = string.Empty;
		private string _password = string.Empty;
		private string _timezone = "America/New_York";
		private int _syncPreviousDays = 7;
		private int _syncFutureDays = 30;
		private bool _exitOnSyncComplete = false;
		private ArrayList _synchronizedEvents = new ArrayList();
		private CalendarService _calendarService = null;
		private DateTime _lastSyncDateTime;
        private bool _isByassNetworkTest = false;
		//Added by Sampath on 12Dec2007 for Proxy Support
		private string _proxyServer = string.Empty;
		private int _proxyPort = 0;
		private bool _useProxy = false;
        private string _syncExclusionList = string.Empty;
		internal delegate void DataEventHandler(GMobileSync sender, string text, int processed, int total);
		internal event DataEventHandler DownloadProgressEvent;

		#endregion

		#region Properties

		public string Username
		{
			get { return this._username; }
			set { this._username = value; }
		}

		public string Password
		{
			get { return this._password; }
			set { this._password = value; }
		}

		public string Timezone
		{
			get { return this._timezone; }
			set { this._timezone = value; }
		}

		public int SyncPreviousDays
		{
			get { return this._syncPreviousDays; }
			set { this._syncPreviousDays = value; }
		}

		public int SyncFutureDays
		{
			get { return this._syncFutureDays; }
			set { this._syncFutureDays = value; }
		}

		public bool ExitOnSyncComplete
		{
			get { return this._exitOnSyncComplete; }
			set { this._exitOnSyncComplete = value; }
		}

		public ArrayList SynchronizedEvents
		{
			get { return this._synchronizedEvents; }
			set { this._synchronizedEvents = value; }
		}

		//Added by Sampath on 28Nov2007 to store the last time Sync was run
		public DateTime LastSyncDateTime
		{
			get { return this._lastSyncDateTime; }
			set { this._lastSyncDateTime = value; }
		}

		public bool IsByassNetworkTest
		{
            get { return this._isByassNetworkTest; }
            set { this._isByassNetworkTest = value; }
		}

		//Added by Sampath on 12Dec2007 for Proxy Support
		public string ProxyServer
		{
			get { return this._proxyServer; }
			set { this._proxyServer = value; }
		}

		public int ProxyPort
		{
			get { return this._proxyPort; }
			set { this._proxyPort = value; }
		}

		public bool UseProxy
		{
			get { return this._useProxy; }
			set { this._useProxy = value; }
		}

        public string SyncExclusionList
        {
            get { return this._syncExclusionList; }
            set { this._syncExclusionList = value; }
        }
		//--------------------------->


		public static string ApplicationDataDirectory
		{
			get
			{
				return System.Environment.GetFolderPath(
						Environment.SpecialFolder.ApplicationData) + "\\GMobileSync";
			}
		}

		public static string DataFile
		{
			get { return ApplicationDataDirectory + "\\" + dataFile; }
		}

		public static bool IsConnected
		{
			get
			{
				bool connected = false;

				try
				{
					IPHostEntry thisHost =
							Dns.GetHostEntry(Dns.GetHostName());
					string localIpAddress =
							thisHost.AddressList[0].ToString();

					connected = localIpAddress !=
							System.Net.IPAddress.Parse("127.0.0.1").ToString();
				}

				catch (Exception)
				{
					connected = false;
				}

				return connected;
			}
		}


		#endregion

		#region Constructors

		public GMobileSync()
		{

		}

		public GMobileSync(string username, string password)
		{
			this._username = username;
			this._password = password;

			SetupGoogleCalendarService();
		}

		private void SetupGoogleCalendarService()
		{
			if (this._username.Equals(string.Empty))
				throw new Exception("Username not specified.");

			this._calendarService = new CalendarService("RareEdge-GMobileSync-13");
			//Added by Sampath on 19Dec2007 to have proxy support
			if (this.UseProxy)
			{
				GDataRequestFactory requestFactory = (GDataRequestFactory)this._calendarService.RequestFactory;

				WebProxy myProxy = new WebProxy(this.ProxyServer, this.ProxyPort);
				myProxy.BypassProxyOnLocal = true;
				myProxy.Credentials = CredentialCache.DefaultCredentials;
				requestFactory.Proxy = myProxy;
			}
			//-----------------------------

			this._calendarService.setUserCredentials(this._username, this._password);
		}

		#endregion

		#region Methods

		/// <summary>
		/// Saves the GMobileSync settings file
		/// </summary>
		public void Save()
		{
			if (!Directory.Exists(ApplicationDataDirectory))
				Directory.CreateDirectory(ApplicationDataDirectory);

			StreamWriter sw = new StreamWriter(DataFile);
			XmlSerializer xml = new XmlSerializer(typeof(GMobileSync));

			xml.Serialize(sw, this);
			sw.Close();
		}

		/// <summary>
		/// Loads the GMobileSync settings file with the previous state
		/// </summary>
		/// <returns>Previous GMobileSync settings</returns>
		public static GMobileSync Load()
		{
			if (!Directory.Exists(ApplicationDataDirectory))
				Directory.CreateDirectory(ApplicationDataDirectory);

			GMobileSync gms = null;
			StreamReader sr = null;

			try
			{
				sr = new StreamReader(DataFile);
				XmlSerializer xs = new XmlSerializer(typeof(GMobileSync));

				gms = (GMobileSync)xs.Deserialize(sr);
			}
			catch (IOException)
			{
				gms = new GMobileSync();
			}
			catch (Exception)
			{
				gms = new GMobileSync();
			}
			finally
			{
				if (sr != null)
					sr.Close();
			}

			return gms;
		}

		/// <summary>
		/// Set a custom property on an Appointment object
		/// </summary>
		/// <param name="appointment">Appointment receiving custom property</param>
		/// <param name="property">Property name</param>
		/// <param name="value">Property value</param>
		/// <param name="create">Should create property if doesn't exist?</param>
		/// <param name="type">Property type</param>
		/// <returns>True with successful set/create, otherwise false</returns>
		public static bool SetCustomProperty(ref Appointment appointment, string property, object value, bool create, Type type)
		{
			try
			{
				bool propertyCanBeSet = true;

				if (appointment != null)
				{
					if (appointment.Properties != null)
					{
						if (!appointment.Properties.Contains(property))
							propertyCanBeSet = create ? appointment.Properties.Add(property, type) : false;

						if (propertyCanBeSet)
							appointment.Properties[property] = value;
						else
							return false;
					}
				}
			}

			catch (Exception)
			{
				return false;
			}

			return true;
		}

		/// <summary>
		/// Gets a custom property on an Appointment object
		/// </summary>
		/// <param name="appointment">Appointment containing custom property</param>
		/// <param name="property">Property name</param>
		/// <returns>Object with custom property value or null on failure/not found</returns>
		public static object GetCustomProperty(Appointment appointment, string property)
		{
			object value = null;

			try
			{
				if (appointment != null)
					if (appointment.Properties != null)
						if (appointment.Properties.Contains(property))
							if (appointment.Properties[property] != null)
								value = appointment.Properties[property];
			}

			catch (Exception)
			{
				value = null;
			}

			return value;
		}

		/// <summary>
		/// Gets Google Calendar entries
		/// </summary>
		/// <returns></returns>
		public ArrayList GetGCalendarEntries()
		{
			return GetGCalendarEntries(this._username, this._password);
		}

		/// <summary>
		/// Determines if the device is really able to connect to the internet and download a small file
		/// </summary>
		/// <returns>Whether the connection worked or not</returns>
		public bool IsReallyConnected()
		{
			WebRequest request = null;
			HttpWebResponse response = null;
			Stream stream = null;
			StreamReader reader = null;
			string responseFromServer = string.Empty;
			bool isReallyConnected = false;

			try
			{
				request = WebRequest.Create("http://rareedge.com/gmobilesync/connected.txt");
				//Added by Sampath on 19Dec2007 to have proxy support
				if (this.UseProxy)
				{
					WebProxy myProxy = new WebProxy(this.ProxyServer, this.ProxyPort);
					myProxy.BypassProxyOnLocal = true;
					myProxy.Credentials = CredentialCache.DefaultCredentials;
					request.Proxy = myProxy;
				}
				//-----------------------------


				response = (HttpWebResponse)request.GetResponse();
				stream = response.GetResponseStream();
				reader = new StreamReader(stream);
				responseFromServer = reader.ReadToEnd();

				isReallyConnected = responseFromServer.Trim().ToLower().Equals("connected");
			}

			catch
			{
				isReallyConnected = false;
			}

			finally
			{
				if (reader != null)
					reader.Close();

				if (stream != null)
					stream.Close();

				if (response != null)
					response.Close();
			}

			return isReallyConnected;
		}

		public static string GenerateChecksum(Microsoft.WindowsMobile.PocketOutlook.Appointment appointment)
		{
			// Generate checksum to detect changes
			// Checksum is: subject, start date, start time, end date, end time, body, location, recipient(s)

            // We need to sort the recipients list, beacuse the sequence in this list may change among syncorizations.
            string recipients = String.Empty;
			ArrayList recipientList = new ArrayList(appointment.Recipients.Count);
            foreach (Recipient recipient in appointment.Recipients)
            {
                recipientList.Add(recipient.Name + recipient.Address);
            }
            recipientList.Sort();
            foreach (string s in recipientList)
            {
                recipients += s;
            }
			string checksum = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}",
					appointment.Subject, appointment.Start.ToShortDateString(), appointment.Start.ToShortTimeString(),
					appointment.End.ToShortDateString(), appointment.End.ToShortTimeString(), appointment.Body,
                    appointment.Location, recipients);

			//return RareEdge.Utility.GetMd5Hash(checksum);
			MD5 cryptoProvider = new MD5CryptoServiceProvider();
			byte[] hash = cryptoProvider.ComputeHash(Encoding.ASCII.GetBytes(checksum));
			string result = "";
			foreach (byte b in hash)
				result += b.ToString("x2");

			return result;
		}

		public static bool CompareChecksumEquality(Microsoft.WindowsMobile.PocketOutlook.Appointment appointment, string existingChecksum)
		{
			string newChecksum = GenerateChecksum(appointment);

			return existingChecksum.Equals(newChecksum);
		}

		/// <summary>
		/// Gets Google Calendar feeds for the provided credentials
		/// </summary>
		/// <param name="username">Google Account username</param>
		/// <param name="password">Google Account password</param>
		/// <returns>A collection of Google Calendar feeds</returns>
		public ArrayList GetGCalendarFeeds(string username, string password)
		{
			ArrayList entries = null;
			EventQuery query = null;
			EventFeed feed = null;

			entries = new ArrayList();

			if (this._calendarService == null)
				SetupGoogleCalendarService();

			query = new EventQuery(RareEdge.Properties.Resources.GCalPrivateFeedsList);

			feed = this._calendarService.Query(query);

			while (feed != null && feed.Entries.Count > 0)
			{
				foreach (EventEntry entry in feed.Entries)
				{
					entries.Add(entry);
				}

				if (feed.NextChunk != null)
				{
					query = new EventQuery(feed.NextChunk);

					feed = this._calendarService.Query(query);
				}
				else
				{
					feed = null;
				}
			}

			return entries;
		}

		/// <summary>
		/// Gets Google Calendar entries for the provided credentials
		/// </summary>
		/// <param name="username">Google Account username</param>
		/// <param name="password">Google Account password</param>
		/// <returns>A collection of Google Calendar entries</returns>
		public ArrayList GetGCalendarEntries(string username, string password)
		{
			ArrayList entries = null;
			ArrayList feedEntries = null;
			EventQuery query = null;
			EventFeed feed = null;

			entries = new ArrayList();

			if (this._calendarService == null)
				SetupGoogleCalendarService();

			FireDownloadProgressEvent("Downloading list of calendars...", 0, 0);

			feedEntries = GetGCalendarFeeds(username, password);

            ArrayList exclusionList = new ArrayList(SyncExclusionList.Split(';'));

			//foreach (EventEntry feedEntry in feedEntries)
			for (int i = 0; i < feedEntries.Count; i++)
			{
                string title = ((EventEntry)feedEntries[i]).Title.Text;
                if (exclusionList.IndexOf(title) != -1)
                {
                    FireDownloadProgressEvent(string.Format("Skipping calendar {0}...", title), i, feedEntries.Count);
                    continue;
                }
                else
                {
				    FireDownloadProgressEvent(string.Format("Downloading calendar {0} of {1}...", i + 1, feedEntries.Count),
																	i, feedEntries.Count);
                }

				query = new EventQuery(((EventEntry)feedEntries[i]).Links[0].AbsoluteUri);
				query.StartTime = DateTime.Now.AddDays(-1 * this._syncPreviousDays);
				query.EndTime = DateTime.Now.AddDays(this._syncFutureDays);

				feed = this._calendarService.Query(query);

				while (feed != null && feed.Entries.Count > 0)
				{
					foreach (EventEntry entry in feed.Entries)
					{
						entries.Add(entry);
					}

					if (feed.NextChunk != null)
					{
						query = new EventQuery(feed.NextChunk);

						feed = this._calendarService.Query(query);
					}
					else
					{
						feed = null;
					}
				}

				FireDownloadProgressEvent(string.Format("Downloading calendar {0} of {1}...", i + 1, feedEntries.Count),
																	i + 1, feedEntries.Count);
			}

			return entries;
		}

		/// <summary>
		/// Create an EventEntry
		/// </summary>
		/// <param name="title">Title</param>
		/// <param name="authorName">Author name</param>
		/// <param name="authorEmail">Author email</param>
		/// <param name="startTime">Start time</param>
		/// <param name="endTime">End time</param>
		/// <param name="fAllDay">Is all day</param>
		/// <param name="location">Location</param>
		/// <param name="body">Body</param>
		/// <returns>An EventEntry that has not been inserted to Google Calendar</returns>
		public EventEntry CreateEvent(string title, string authorName, string authorEmail, DateTime startTime, DateTime endTime, bool fAllDay, string location, string body)
		{
			EventEntry entry = new EventEntry();
			AtomPerson person = new AtomPerson(AtomPersonType.Author, authorName);
			Where newPlace = new Where();
			When newTime = new When();

			entry.Title = new AtomTextConstruct(AtomTextConstructElementType.Title, title);
			person.Email = authorEmail;
			entry.Authors.Add(person);
			entry.Published = DateTime.Now;
			entry.Updated = DateTime.Now;
			newPlace.ValueString = location;
			entry.Locations.Add(newPlace);
			newTime.StartTime = startTime;
			newTime.EndTime = endTime;
			newTime.AllDay = fAllDay;
			entry.Times.Add(newTime);
			entry.Content.Content = body;



			return entry as EventEntry;
		}

		/// <summary>
		/// Gets an event from Google by the SelfUri
		/// </summary>
		/// <param name="selfUri">SelfUri of event</param>
		/// <returns>An EventEntry based on the SelfUri</returns>
		public EventEntry GetEvent(string selfUri)
		{
			if (this._calendarService == null)
			{
				SetupGoogleCalendarService();
			}

			FeedQuery query = new FeedQuery(selfUri);
			EventEntry entry;
			AtomFeed feed = this._calendarService.Query(query);

			entry = feed.Entries[0] as EventEntry;

			return entry;
		}

		/// <summary>
		/// Insert an event to Google Calendar and return the event
		/// </summary>
		/// <param name="entry">A populated EventEntry</param>
		/// <returns>The inserted EventEntry</returns>
		public EventEntry InsertEvent(EventEntry entry)
		{
			if (this._calendarService == null)
			{
				SetupGoogleCalendarService();
			}

			EventQuery query = new EventQuery(RareEdge.Properties.Resources.GCalPrivateFullUri);
			EventFeed feed = this._calendarService.Query(query);

			return feed.Insert(entry) as EventEntry;
		}

		/// <summary>
		/// Insert an event to Google Calendar and return the event
		/// </summary>
		/// <param name="title">Title</param>
		/// <param name="authorName">Author name</param>
		/// <param name="authorEmail">Author email</param>
		/// <param name="startTime">Start time</param>
		/// <param name="endTime">End time</param>
		/// <param name="fAllDay">Is all day</param>
		/// <param name="location">Location</param>
		/// <param name="body">Body</param>
		/// <returns>The inserted EventEntry</returns>
		public EventEntry InsertEvent(string title, string authorName, string authorEmail, DateTime startTime, DateTime endTime, bool fAllDay, string location, string body)
		{
			EventEntry entry = CreateEvent(title, authorName, authorEmail, startTime, endTime, fAllDay, location, body);

			return InsertEvent(entry);
		}

		public EventEntry[] InsertEvents(EventEntry[] events)
		{
			if (this._calendarService == null)
			{
				SetupGoogleCalendarService();
			}
            
            // We cannot use "default" URI to create new events, 
            // otherwise, the event URI returned will be different 
            // with the URI returned when getting calendar entries 
            // next time we synchronize. Which causes a local created
            // appointment get duplicated after next synchronization.
            string calendarUri = RareEdge.Properties.Resources.GCalPrivateFullUri.Replace("/default/", "/" + Username.Replace("@", "%40")  + "/");
			EventQuery query = new EventQuery(calendarUri);
			EventFeed feed = this._calendarService.Query(query);
			EventEntry[] insertedEvents = new EventEntry[events.Length];
			int index = 0;

			foreach (EventEntry entry in events)
			{
				try
				{
					insertedEvents[index] = feed.Insert(entry) as EventEntry;
					//Try Catch Added by Sampath on 02dec2007.
					//This was added to because, if a insert fails for one item, then the rest is ignored and also
					//it jumps out of updating the local appointment status of the updates.


					//Update the outlook appointments SelfUri property so we know where the event is on Google
					index++;
				}
				catch (Exception)
				{
					continue;
				}
			}

			return insertedEvents;
		}

		/// <summary>
		/// Finds a previously synchronized event
		/// </summary>
		/// <param name="atomEventId">Google Calendar Atom event ID</param>
		/// <returns>The PocketOutlook PIM ID</returns>
		public string FindPreviouslySynchronizedEvent(string atomEventId)
		{
			foreach (string item in this._synchronizedEvents)
			{
				string[] ids = item.Split(',');

				if (ids[1].Equals(atomEventId))
					return ids[0];
			}

			return null;
		}

		/// <summary>
		/// Mergest synchronization lists (used to prevent corruption of list after a cancelled sync)
		/// </summary>
		/// <param name="mergeList">List to merge with the previous list</param>
		/// <returns>Merged list</returns>
		public ArrayList MergeSynchronizationList(ArrayList mergeList)
		{
			ArrayList merged = new ArrayList();

			foreach (string mergeItem in mergeList)
			{
				bool found = false;

				foreach (string existingItem in this._synchronizedEvents)
				{
					if (existingItem.Equals(mergeItem))
					{
						found = true;
						break;
					}
				}

				if (!found)
					merged.Add(mergeItem);
			}

			merged.AddRange(this._synchronizedEvents);

			return merged;
		}

		public static string CreateRecurrenceRule(Appointment appointment, string timezone)
		{
			string rule = string.Empty;
			string date = appointment.Start.ToString("yyyyMMdd");
			string time = appointment.Start.ToString("HHmmss");
			string weekStartDay = System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)System.Globalization.DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek].ToUpper();
			string weekdays = string.Empty;
			string months = string.Empty;
			string freq = string.Empty;
			string interval = string.Empty;

			TimeSpan duration = appointment.End.Subtract(appointment.Start);

			if (appointment.RecurrencePattern.RecurrenceType == RecurrenceType.Weekly)
			{
				freq = "WEEKLY";

				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Sunday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add sunday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Sunday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Monday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add monday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Monday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Tuesday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add tuesday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Tuesday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Wednesday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add wednesday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Wednesday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Thursday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add thursday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Thursday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Friday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add friday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Friday] + ",";
				}
				if ((appointment.RecurrencePattern.DaysOfWeekMask | DaysOfWeek.Saturday) == appointment.RecurrencePattern.DaysOfWeekMask)
				{
					// add saturday 
					weekdays += System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortestDayNames[(int)DayOfWeek.Saturday] + ",";
				}

				if (weekdays.Length > 0)
					weekdays = weekdays.Substring(0, weekdays.Length - 1);

				weekdays = "BYDAY=" + weekdays.ToUpper() + ";";
			}
			else if (appointment.RecurrencePattern.RecurrenceType == RecurrenceType.Monthly)
			{
				freq = "MONTHLY";

			}
			else if (appointment.RecurrencePattern.RecurrenceType == RecurrenceType.Yearly)
			{
				freq = "YEARLY";

			}
			else if (appointment.RecurrencePattern.RecurrenceType == RecurrenceType.Daily)
			{
				freq = "DAILY";
			}

			if (appointment.RecurrencePattern.RecurrenceType != RecurrenceType.Yearly && appointment.RecurrencePattern.RecurrenceType != RecurrenceType.YearByNumber && appointment.RecurrencePattern.RecurrenceType != RecurrenceType.NoRecurrence)
				interval = "INTERVAL=" + appointment.RecurrencePattern.Interval + ";";


			rule = string.Format(@"DTSTART;TZID={0}:{1}T{2}
DURATION:PT{3}S
RRULE:FREQ={4};{5}{6}WKST={7}", timezone, date, time, duration.TotalSeconds, freq, weekdays, interval, weekStartDay);



			return rule;
		}

		internal static When GetEventEntryEarliestTime(ExtensionCollection<When> eventTimes)
		{
			When eventEarliestTime = null;

			if ((eventTimes != null) && (eventTimes.Count > 0))
			{
				eventEarliestTime = eventTimes[0];

				foreach (When when in eventTimes)
				{
					if (when.StartTime.CompareTo(eventEarliestTime.StartTime) < 0)
					{
						eventEarliestTime = when;
					}
				}
			}

			return eventEarliestTime;
		}

		private void FireDownloadProgressEvent(string text, int processed, int total)
		{
			if (DownloadProgressEvent != null)
			{
				DownloadProgressEvent(this, text, processed, total);
			}
		}

		#endregion
	}
}
